using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using SpellCore;
using System.ComponentModel;
using System.Drawing;
using System.Data;

namespace SpellerExampleClient
{
    [ProvideProperty("SpellCheckerSystem",typeof(RichTextBox))]
    public class RichTextBoxSpellingEnabler : IExtenderProvider
    {
        private RichTextBox _currentTextBox;
        private int _positionNeedingChecking = 0;
        //private bool _inSpellingContextMenu = false;
        //private bool _spellCheckingOn = false;
        private string _writingSystemId;

        public RichTextBoxSpellingEnabler(Form form)
        {
            foreach (Control control in form.Controls)
            {
                if(control is RichTextBox)
                {
                    RichTextBox textBox = (RichTextBox) control;
                    _currentTextBox = textBox;
                    textBox.TextChanged += new EventHandler(SpellCheckPositionNeedingChecking);
                    textBox.LostFocus += new EventHandler(SpellCheckPositionNeedingChecking);
                    textBox.SelectionChanged += new EventHandler(OnSelectionChanged);
                    textBox.MouseDown += new MouseEventHandler(OnMouseDown);
                }
            }
        }

        public RichTextBoxSpellingEnabler(RichTextBox textBox,string WritingSystemId)
        {
            SetSpellCheckerSystem(textBox, WritingSystemId);
        }

        public RichTextBoxSpellingEnabler(object textBox, string WritingSystemId)
        {
            
        }

        public string WritingSystemId
        {
            get { return _writingSystemId; }
            set
            {
                _writingSystemId = value;
                _spellSession = new AggregatedSpellSession(WritingSystemId);
                
            }
        }

        public string GetSpellCheckerSystem(out object sender)
        {
            sender = _currentTextBox;
            return WritingSystemId;
        }

        public void SetSpellCheckerSystem(object sender, string writingSystemId)
        {
            RichTextBox textBox = (RichTextBox)sender;
            if (!String.IsNullOrEmpty(writingSystemId) && _currentTextBox == null && textBox != null)
            {
                _currentTextBox = textBox;
                textBox.TextChanged += new EventHandler(SpellCheckPositionNeedingChecking);
                textBox.LostFocus += new EventHandler(SpellCheckPositionNeedingChecking);
                textBox.SelectionChanged += new EventHandler(OnSelectionChanged);
                textBox.MouseDown += new MouseEventHandler(OnMouseDown);
            }
            if (_currentTextBox.Equals(textBox))
            {
                WritingSystemId = writingSystemId;
                _spellSession = new AggregatedSpellSession(WritingSystemId);
            }
        }

        private void OnMouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button == System.Windows.Forms.MouseButtons.Right)
            {
                ShowSpellingContextMenu(sender,e.Location);
            }
        }

        internal void ShowSpellingContextMenu(object sender,System.Drawing.Point location)
        {
            RichTextBox sndr = (RichTextBox)sender;
            //_inSpellingContextMenu = true;
            sndr.Select(sndr.GetCharIndexFromPosition(location), 0);

            int wordBeingEditedStartIndex;
            string wordBeingEdited = GetWordFromPosition(sndr.Text, sndr.SelectionStart, out wordBeingEditedStartIndex);
            CheckingResult result = CheckAndUpdateDisplayForWord(sndr,wordBeingEdited, wordBeingEditedStartIndex);
            sndr.ContextMenuStrip = new ContextMenuStrip();
            _currentRichTextBoxWithContextMenu = sndr;

            if (result != CheckingResult.Good)
            {
                foreach (SimilarityCheckResult suggestion in _spellSession.GetSuggestions(wordBeingEdited))
                {
                    sndr.ContextMenuStrip.Items.Add(suggestion.Word);
                }
            }
            sndr.ContextMenuStrip.Items.Add(new ToolStripSeparator());
            sndr.ContextMenuStrip.Items.Add("Add to Dictionary");

            Font contextMenuFont = new Font("Tahoma", 12);
            sndr.ContextMenuStrip.Font = contextMenuFont;
            sndr.ContextMenuStrip.ItemClicked += new ToolStripItemClickedEventHandler(_context_ItemClicked);
            //_inSpellingContextMenu = false;
        }

        internal void _context_ItemClicked(object sender, ToolStripItemClickedEventArgs e)
        {
            ContextMenuStrip sndr = (ContextMenuStrip)sender;
            int wordBeingEditedStartIndex;
            string wordBeingEdited = GetWordFromPosition(_currentRichTextBoxWithContextMenu.Text, 
                                            _currentRichTextBoxWithContextMenu.SelectionStart, out wordBeingEditedStartIndex);
            _currentRichTextBoxWithContextMenu.Select(wordBeingEditedStartIndex, wordBeingEdited.Length);

            if (e.ClickedItem.Text.Equals("Add to Dictionary"))
            {
                _spellSession.AddGoodWord(_currentRichTextBoxWithContextMenu.SelectedText);
                return;
            }

            _currentRichTextBoxWithContextMenu.SelectedText = e.ClickedItem.Text;
        }

        private AggregatedSpellSession _spellSession;

        internal AggregatedSpellSession AggregateSpellingSession
        {
            get { return _spellSession; }
            set { _spellSession = value; }
        }

        private void OnSelectionChanged(object sender,EventArgs e)
        {
            RichTextBox sndr = (RichTextBox)sender;
            if (sndr.SelectionLength == 0)
            {
                SpellCheckPositionNeedingChecking(sender,e);
            }
        }

        bool _inSpellCheckPositionNeedingChecking = false;
        private RichTextBox _currentRichTextBoxWithContextMenu;

        private void SpellCheckPositionNeedingChecking(object sender,EventArgs e)
        {
            RichTextBox sndr = (RichTextBox) sender;
            if (_inSpellCheckPositionNeedingChecking) // no reentry!
            {
                return;
            }

            if (_spellSession == null || String.IsNullOrEmpty(sndr.Text))
            {
                return;
            }
            _inSpellCheckPositionNeedingChecking = true;

            // don't do anything if the position needing checking is in the same word as our current selection
            int wordToCheckStartIndex, wordBeingEditedStartIndex;
            string wordToCheck = GetWordFromPosition(sndr.Text, _positionNeedingChecking, out wordToCheckStartIndex);
            //SelectionStart == current cursor position
            string wordBeingEdited = GetWordFromPosition(sndr.Text, sndr.SelectionStart, out wordBeingEditedStartIndex);

            int originalSelectionStart = sndr.SelectionStart;
            int originalSelectionLength = sndr.SelectionLength;

            if (sndr.SelectionLength == 0 && (sndr.SelectionStart > 0 && sndr.Text[sndr.SelectionStart - 1] == ' '))
            {
                sndr.Select(sndr.SelectionStart - 1, 1);
                SetUnderlineStyleGivenRichTextBox(sndr, UnderlineStyle.None);
                sndr.Select(originalSelectionStart, originalSelectionLength);
                SetUnderlineStyleGivenRichTextBox(sndr, UnderlineStyle.None);
            }
            if (wordBeingEdited.Length > 0)
            {
                bool wordBeingEditedIsMisspelled = false;
                sndr.Select(wordBeingEditedStartIndex, 1);
                if (GetUnderlineStyleGivenRichTextBox(sndr) == UnderlineStyle.Wave)
                {
                    wordBeingEditedIsMisspelled = true;
                }
                else
                {
                    sndr.Select(wordBeingEditedStartIndex + wordBeingEdited.Length - 1, 1);
                    if (GetUnderlineStyleGivenRichTextBox(sndr) == UnderlineStyle.Wave)
                    {
                        wordBeingEditedIsMisspelled = true;
                    }
                }

                if (wordBeingEditedIsMisspelled)
                {
                    CheckAndUpdateDisplayForWord(sndr,wordBeingEdited, wordBeingEditedStartIndex);
                }
            }

            if (wordBeingEditedStartIndex != wordToCheckStartIndex)
            {
                CheckAndUpdateDisplayForWord(sndr,wordToCheck, wordToCheckStartIndex);
            }

            sndr.Select(originalSelectionStart, originalSelectionLength);

            //SelectionUnderlineStyle = UnderlineStyle.None;

            _positionNeedingChecking = sndr.SelectionStart;
            _inSpellCheckPositionNeedingChecking = false;
        }

        private CheckingResult CheckAndUpdateDisplayForWord(RichTextBox textBox,string wordToCheck, int wordToCheckStartIndex)
        {
            int originalSelectionStart = textBox.SelectionStart;
            int originalSelectionLength = textBox.SelectionLength;

            textBox.Select(wordToCheckStartIndex, wordToCheck.Length);
            CheckingResult result = _spellSession.GetWordStatus(wordToCheck);
            switch (result)
            {
                case CheckingResult.Unknown:
                case CheckingResult.StructurallyUnsound:
                case CheckingResult.Verify:
                case CheckingResult.Bad:
                    SetUnderlineStyleGivenRichTextBox(textBox,UnderlineStyle.Wave);
                    SetUnderlineColorGivenRichTextBox(textBox, UnderlineColor.Red);
                    break;
                case CheckingResult.Good:
                    SetUnderlineStyleGivenRichTextBox(textBox, UnderlineStyle.None);
                    break;
                default:
                    break;
            }
            textBox.Select(originalSelectionStart, originalSelectionLength);
            //SelectionUnderlineStyle = UnderlineStyle.None;
            return result;
        }

        private static string GetWordFromPosition(string text, int position, out int wordStartIndex)
        {
            int start;
            if (position > text.Length)
            {
                position = text.Length;
            }
            if (position == 0)
            {
                start = -1;
            }
            else
            {
                start = text.LastIndexOfAny(new char[] { ' ', '\t', '\n' }, position - 1);
            }
            ++start;
            wordStartIndex = start; // cursor happens after

            if (start > text.Length)
            {
                start = text.Length;
            }
            int end = text.IndexOfAny(new char[] { ' ', '\t', '\n' }, position);
            if (end == -1)
            {
                end = text.Length;
            }
            string word = text.Substring(start, end - start);
            return word;
        }

        #region UnderLineStyle

        private const int SCF_SELECTION = 1;

        private const int CFM_UNDERLINETYPE = 8388608;
        private const int CFE_UNDERLINE = 4;

        private const int EM_SETCHARFORMAT = 1092;
        private const int EM_GETCHARFORMAT = 1082;

        [StructLayout(LayoutKind.Sequential)]
        private struct CHARFORMAT
        {
            public int cbSize;
            public uint dwMask;
            public uint dwEffects;
            public int yHeight;
            public int yOffset;
            public int crTextColor;
            public byte bCharSet;
            public byte bPitchAndFamily;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
            public char[] szFaceName;

            // CHARFORMAT2 from here onwards.
            public short wWeight;
            public short sSpacing;
            public int crBackColor;
            public int LCID;
            public uint dwReserved;
            public short sStyle;
            public short wKerning;
            public byte bUnderlineType;
            public byte bAnimation;
            public byte bRevAuthor;
        }

        [DllImport("user32", CharSet = CharSet.Auto)]
        private static extern int SendMessage(HandleRef hWnd, int msg,
                                               int wParam, ref CHARFORMAT lp);



        // <summary>
        /// Gets or sets the underline style to apply to the
        /// current selection or insertion point.
        /// </summary>
        /// <remarks>
        /// Underline styles can be set to any value of the
        /// <see cref="UnderlineStyle"/> enumeration.
        /// </remarks>

        internal static UnderlineStyle GetUnderlineStyleGivenRichTextBox(RichTextBox textBox)
        {
            CHARFORMAT fmt = new CHARFORMAT();
            fmt.cbSize = Marshal.SizeOf(fmt);

            // Get the underline style.
            SendMessage(new HandleRef(textBox, textBox.Handle), EM_GETCHARFORMAT,
                         SCF_SELECTION, ref fmt);

            // Default to no underline.
            if ((fmt.dwMask & CFM_UNDERLINETYPE) == 0)
                return UnderlineStyle.None;

            if ((fmt.dwEffects & CFE_UNDERLINE) == 0)
                return UnderlineStyle.None;

            byte style = (byte)(fmt.bUnderlineType & 0x0F);

            return (UnderlineStyle)style;
        }

        internal static void SetUnderlineStyleGivenRichTextBox(RichTextBox textBox,UnderlineStyle style)
        {
            // Ensure we don't alter the color by accident.
            UnderlineColor color = GetUnderlineColorGivenRichTextBox(textBox);

            // Ensure we don't show it if it shouldn't be shown.
            if (style == UnderlineStyle.None)
                color = UnderlineColor.Black;

            CHARFORMAT fmt = new CHARFORMAT();
            fmt.cbSize = Marshal.SizeOf(fmt);
            fmt.dwMask = CFM_UNDERLINETYPE;
            if (style != UnderlineStyle.None)
            {
                fmt.dwEffects = CFE_UNDERLINE;
            }
            fmt.bUnderlineType = (byte)((byte)style | (byte)color);

            // Set the underline type.
            SendMessage(new HandleRef(textBox, textBox.Handle), EM_SETCHARFORMAT,
                         SCF_SELECTION, ref fmt);
        }


        /// <summary>
        /// Specifies the style of underline that should be
        /// applied to the text.
        /// </summary>
        public enum UnderlineStyle
        {
            /// <summary>
            /// No underlining.
            /// </summary>
            None = 0,

            /// <summary>
            /// Standard underlining across all words.
            /// </summary>
            Normal = 1,

            /// <summary>
            /// Standard underlining broken between words.
            /// </summary>
            Word = 2,

            /// <summary>
            /// Double line underlining.
            /// </summary>
            Double = 3,

            /// <summary>
            /// Dotted underlining.
            /// </summary>
            Dotted = 4,

            /// <summary>
            /// Dashed underlining.
            /// </summary>
            Dash = 5,

            /// <summary>
            /// Dash-dot ("-.-.") underlining.
            /// </summary>
            DashDot = 6,

            /// <summary>
            /// Dash-dot-dot ("-..-..") underlining.
            /// </summary>
            DashDotDot = 7,

            /// <summary>
            /// Wave underlining (like spelling mistakes in MS Word).
            /// </summary>
            Wave = 8,

            /// <summary>
            /// Extra thick standard underlining.
            /// </summary>
            Thick = 9,

            /// <summary>
            /// Extra thin standard underlining.
            /// </summary>
            HairLine = 10,

            /// <summary>
            /// Double thickness wave underlining.
            /// </summary>
            DoubleWave = 11,

            /// <summary>
            /// Thick wave underlining.
            /// </summary>
            HeavyWave = 12,

            /// <summary>
            /// Extra long dash underlining.
            /// </summary>
            LongDash = 13
        }

        /// <summary>
        /// Gets or sets the underline color to apply to the
        /// current selection or insertion point.
        /// </summary>
        /// <remarks>
        /// Underline colors can be set to any value of the
        /// <see cref="UnderlineColor"/> enumeration.
        /// </remarks>

        internal static UnderlineColor GetUnderlineColorGivenRichTextBox(RichTextBox textBox)
        {
                CHARFORMAT fmt = new CHARFORMAT();
                fmt.cbSize = Marshal.SizeOf(fmt);

                // Get the underline color.
                SendMessage(new HandleRef(textBox, textBox.Handle), EM_GETCHARFORMAT,
                             SCF_SELECTION, ref fmt);

                // Default to black.
                if ((fmt.dwMask & CFM_UNDERLINETYPE) == 0)
                    return UnderlineColor.Black;

                if ((fmt.dwEffects & CFE_UNDERLINE) == 0)
                    return UnderlineColor.Black;

                byte style = (byte)(fmt.bUnderlineType & 0xF0);

                return (UnderlineColor)style;
            }

        internal static void SetUnderlineColorGivenRichTextBox(RichTextBox textBox,UnderlineColor color)
        {
                // Ensure we don't alter the style.
                UnderlineStyle style = GetUnderlineStyleGivenRichTextBox(textBox);

                // Ensure we don't show it if it shouldn't be shown.
                if (style == UnderlineStyle.None)
                    color = UnderlineColor.Black;

                CHARFORMAT fmt = new CHARFORMAT();
                fmt.cbSize = Marshal.SizeOf(fmt);
                fmt.dwMask = CFM_UNDERLINETYPE;
                if (style != UnderlineStyle.None)
                {
                    fmt.dwEffects = CFE_UNDERLINE;
                }

                fmt.bUnderlineType = (byte)((byte)style | (byte)color);

                // Set the underline color.
                SendMessage(new HandleRef(textBox, textBox.Handle), EM_SETCHARFORMAT,
                             SCF_SELECTION, ref fmt);
        }

        /// <summary>
        /// Specifies the color of underline that should be
        /// applied to the text.
        /// </summary>
        /// <remarks>
        /// I named these colors by their appearance, so some
        /// of them might not be what you expect. Please email
        /// me if you feel one should be changed.
        /// </remarks>
        public enum UnderlineColor
        {
            /// <summary>Black.</summary>
            Black = 0x00,

            /// <summary>Blue.</summary>
            Blue = 0x10,

            /// <summary>Cyan.</summary>
            Cyan = 0x20,

            /// <summary>Lime green.</summary>
            LimeGreen = 0x30,

            /// <summary>Magenta.</summary>
            Magenta = 0x40,

            /// <summary>Red.</summary>
            Red = 0x50,

            /// <summary>Yellow.</summary>
            Yellow = 0x60,

            /// <summary>White.</summary>
            White = 0x70,

            /// <summary>DarkBlue.</summary>
            DarkBlue = 0x80,

            /// <summary>DarkCyan.</summary>
            DarkCyan = 0x90,

            /// <summary>Green.</summary>
            Green = 0xA0,

            /// <summary>Dark magenta.</summary>
            DarkMagenta = 0xB0,

            /// <summary>Brown.</summary>
            Brown = 0xC0,

            /// <summary>Olive green.</summary>
            OliveGreen = 0xD0,

            /// <summary>Dark gray.</summary>
            DarkGray = 0xE0,

            /// <summary>Gray.</summary>
            Gray = 0xF0
        }

        #endregion

        #region IExtenderProvider Members

        public bool CanExtend(object extendee)
        {
            return extendee is RichTextBox;
        }

        #endregion
    }
}
